package m3u8

import (
	"context"
	"errors"
	"fmt"
	"github.com/wuzfei/go-helper/slices"
	"go.uber.org/zap"
	"io"
	"onvif_server/internal/utils"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

var ErrorM3u8DirNotExists = errors.New("m3u8 dir not exists")

type M3u8 struct {
	CamRootPath   string
	log           *zap.Logger
	localTimeZone *time.Location
}

func NewM3u8(camRootPath string, log *zap.Logger, loc *time.Location) *M3u8 {
	return &M3u8{
		CamRootPath:   camRootPath,
		log:           log.Named("m3u8"),
		localTimeZone: loc,
	}
}

// DayM3u8SplitByHours 指定某一天按小时生成m3u8文件
func (m3 *M3u8) DayM3u8SplitByHours(deviceId string, year int, month int, day int) ([]string, error) {
	ret := make([]string, 0)
	for i := 0; i < 24; i++ {
		p, err := m3.GenM3u8byHour(deviceId, year, month, day, i)
		if err == nil {
			ret = append(ret, p)
		} else {
			ret = append(ret, "")
		}
	}
	return ret, nil
}

// GenM3u8byHour 按小时生成m3u8文件
func (m3 *M3u8) GenM3u8byHour(deviceId string, year int, month int, day int, hour int) (retHourM3u8 string, err error) {
	tempDir, err := m3.getOrCreateCamDir(deviceId, "tmp")
	if err != nil {
		return
	}

	dayM3u8 := filepath.Join(m3.CamRootPath, deviceId, fmt.Sprintf("%4d%02d%02d.m3u8", year, month, day))
	hourM3u8 := filepath.Join(tempDir, fmt.Sprintf("%4d%02d%02d%02d.m3u8", year, month, day, hour))
	retHourM3u8 = path.Join("/video", deviceId, "tmp", fmt.Sprintf("%4d%02d%02d%02d.m3u8", year, month, day, hour))

	newLines := make([]string, 0)
	bFind := false
	strEXTINF := ""
	tsPath := ""
	cmpTime := fmt.Sprintf("%4d%02d%02d%02d", year, month, day, hour)

	err = utils.FileReadLineString(dayM3u8, func(line string) error {
		//分析每行数据
		if strings.Contains(line, "#EXTINF") {
			strEXTINF = line
		} else if strings.Contains(line, ".ts") {
			if strings.Contains(line, cmpTime) {
				bFind = true
				tsPath = "../" + line
			} else {
				if bFind {
					return io.EOF
				}
			}
		} else if strings.Contains(line, "#EXT-X-DISCONTINUITY") && bFind {
			newLines = append(newLines, line)
		} else if strings.Contains(line, "#EXTM3U") {
			newLines = append(newLines, line)
		} else if strings.Contains(line, "#EXT-X-VERSION") {
			newLines = append(newLines, line)
		} else if strings.Contains(line, "#EXT-X-TARGETDURATION") {
			newLines = append(newLines, line)
		} else if strings.Contains(line, "#EXT-X-MEDIA-SEQUENCE") {
			newLines = append(newLines, line)
		}
		if bFind {
			if len(strEXTINF) != 0 && len(tsPath) != 0 {
				newLines = append(newLines, strEXTINF)
				newLines = append(newLines, tsPath)
				strEXTINF = ""
				tsPath = ""
			}
		}
		return nil
	})

	if err != nil && err != io.EOF {
		return
	}

	if !bFind {
		err = fmt.Errorf("ts[%s] not found", cmpTime)
		return
	}

	newLines = append(newLines, "#EXT-X-ENDLIST")
	err = os.WriteFile(hourM3u8, []byte(strings.Join(newLines, "\n")), 0664)
	if err != nil {
		err = fmt.Errorf("hourM3u8[%s]  write failed: %s", hourM3u8, err)
	}
	return
}

// ParseM3u8toMp4 返回mp4文件
// 输入文件名
// 返回 mp4url seek值(视频偏移值)  时间
// fileKey: deviceid_YY_mm_dd_HH_MM_SS
func (m3 *M3u8) ParseM3u8toMp4(strDeviceID string, tsFile time.Time) (string, float64, string, error) {
	//TODO 当天时间的不要合成mp4
	if time.Now().Format(utils.FormatDatetime2) == tsFile.Format(utils.FormatDatetime2) {
		m3.log.Error("WARNING: The time is too close")
		return "", 0.0, "", errors.New("The time is too close")
	}

	strDate := tsFile.Format(utils.FormatDatetime)
	m3u8file := filepath.Join(m3.CamRootPath, strDeviceID, tsFile.Format(utils.FormatDate1)+".m3u8")

	urlMp4 := ""
	var lsatTs *Ts
	bFind := false

	err := utils.FileReadLineString(m3u8file, func(line string) error {
		m3Ts, _err := ParseM3u8TsName(line, m3.localTimeZone)
		//非ts跳过
		if _err != nil {
			return nil
		}
		if m3Ts.Time().After(tsFile) {
			if m3Ts.Time().After(tsFile.Add(time.Minute * 2)) {
				// 查找的时间比当前的大于2分钟
				return io.EOF
			}
			bFind = true
			if lsatTs == nil {
				lsatTs = &m3Ts
			}
			return io.EOF
		}
		lsatTs = &m3Ts
		return nil
	})

	if err != nil && err != io.EOF {
		return "", 0.0, "", err
	}

	if bFind {
		//2023/03/29/20230329101051.ts
		//  ffmpeg -i video.ts -c:v copy  -an  -y video.mp4
		// ffmpeg /data_dir/video/strDeviceID/LsatTsName
		// ffmpeg /data_dir/video/live/strDeviceID/LsatTsName
		tsPath := filepath.Join(m3.CamRootPath, strDeviceID, lsatTs.String())
		m3.log.Info("ts path", zap.String("tsPath", tsPath))
		b, err := utils.FileExists(tsPath)
		if err != nil {
			return "", 0.0, "", err
		}
		if !b {
			m3.log.Error("tsPath not exist ", zap.String("tsPath", tsPath))
			return "", 0.0, "", errors.New(fmt.Sprintf("tsPath not exist %s", tsPath))
		}

		mp4Dir, err := m3.getOrCreateCamDir(strDeviceID, "tmp")
		if err != nil {
			return "", 0.0, "", err
		}

		mp4Path := filepath.Join(mp4Dir, lsatTs.BaseName()+".mp4")
		m3.log.Info("mp4Path", zap.String("mp4Path", mp4Path))
		b, err = utils.FileExists(mp4Path)
		if err != nil {
			return "", 0.0, "", err
		}
		if !b {
			_, err = utils.RunCmd(context.Background(), "/usr/bin/ffmpeg", "-i", tsPath, "-c:v", "copy", "-an", "-y", mp4Path)
			if err != nil {
				m3.log.Error(" merge video failed ", zap.Strings("cmd", []string{"/usr/bin/ffmpeg", "-i", tsPath, "-c:v", "copy", "-an", "-y", mp4Path}), zap.Error(err))
				return "", 0.0, "", errors.New(fmt.Sprintf("megrate video failed"))
			}
		}
		urlMp4 = fmt.Sprintf("/video/%s/tmp/%s.mp4", strDeviceID, lsatTs.BaseName())
	}

	return urlMp4, 0.0, strDate, nil
}

// CreateCoverImg 给历史视频的每个ts段生成一个封面
// 获取所有的m3u8文件
// 从文件中获取ts记录
// 检测ts记录对应的封面是否存在
// 否则调用ffmpeg生成封面
func (m3 *M3u8) CreateCoverImg(deviceId string) error {
	//TODO 只处理当天且12个小时内的
	currTime := time.Now().Add(-12 * time.Hour)
	currTimes := currTime.Format(utils.FormatDate1)
	return m3.foreachM3u8File(filepath.Join(m3.CamRootPath, deviceId), func(filename string, dir string) error {
		//TODO 只处理当天且12个小时内的
		if strings.Compare(currTimes, strings.TrimSuffix(filename, ".m3u8")) != 0 {
			return nil
		}
		err := m3.foreachTs(filepath.Join(dir, filename), func(ts Ts) error {
			if !ts.Time().After(currTime) {
				return nil
			}
			coverImg := filepath.Join(dir, ts.String()) + ".jpg"
			b, err := utils.FileExists(coverImg)
			if err != nil {
				return nil
			}
			if !b {
				err = utils.GetSnapshot(filepath.Join(dir, ts.String()), coverImg, 1)
				if err != nil {
					m3.log.Error("CreateCoverImg utils.GetSnapshot error", zap.String("img", coverImg), zap.Error(err))
					return nil
				}
			}
			return nil
		})

		if err != nil {
			m3.log.Error("ERROR read m3u8 failed", zap.String("m3u8File", filepath.Join(dir, filename)), zap.Error(err))
		}
		return nil
	})
}

type HistorySt struct {
	Path     string `json:"path"`
	Img      string `json:"img"`
	Duration int32  `json:"duration"`
}

func (m3 *M3u8) History(deviceId string) ([]string, [][2]string, []HistorySt, error) {
	ret := make([][2]string, 0)
	retOld := make([]string, 0)
	retNew := make([]HistorySt, 0)

	err := m3.foreachM3u8File(filepath.Join(m3.CamRootPath, deviceId), func(filename string, dir string) error {
		m3u8File := filepath.Join(dir, filename)
		imgUrl := ""
		err := m3.foreachTs(m3u8File, func(ts Ts) error {
			hour := ts.Time().Hour()
			if hour < 9 || hour > 18 {
				return nil
			}
			url := filepath.Join(dir, ts.String()) + ".jpg"
			b, err := utils.FileExists(url)
			if err != nil {
				return err
			}
			if !b {
				err = utils.GetSnapshot(filepath.Join(dir, ts.String()), url, 3)
				if err != nil {
					m3.log.Error("History utils.GetSnapshot error", zap.String("img", url), zap.Error(err))
				} else {
					imgUrl = "/video/" + deviceId + "/" + ts.String() + ".jpg"
				}
			} else {
				imgUrl = "/video/" + deviceId + "/" + ts.String() + ".jpg"
			}
			//一个文件第一个就够了，这里直接退出
			return io.EOF
		})
		if err != nil {
			return err
		}

		d, _ := m3.getVideoTime(m3u8File)
		retNew = append(retNew, HistorySt{Path: "/video/" + deviceId + "/" + filename, Img: imgUrl, Duration: d})
		retOld = append(retOld, filename)
		tmp := [2]string{"/video/" + deviceId + "/" + filename, imgUrl}
		ret = append(ret, tmp)
		return nil
	})
	return retOld, ret, retNew, err
}

// SearchVideo 将一个时间段内的m3u8, 重新按 minutes 每份切割成小的m3u8文件
func (m3 *M3u8) SearchVideo(deviceId string, tr utils.TimeRange, minutes int) (res []HistoryFile, err error) {
	beginDate := tr.StartTime.Format(utils.FormatDate1) // YYYYmmdd
	endDate := tr.EndTime.Format(utils.FormatDate1)     // YYYYmmdd
	//从m3u8文件读取的符合条件的ts信息
	tsList := make([][2]string, 0)
	//获取m3u8文件列表
	err = m3.foreachM3u8File(filepath.Join(m3.CamRootPath, deviceId), func(filename string, dir string) error {
		dateName := strings.TrimSuffix(filename, ".m3u8")
		//只读取在这个时间段内的m3u8
		if strings.Compare(dateName, beginDate) >= 0 && strings.Compare(dateName, endDate) <= 0 {
			bFind := false
			strEXTINF := ""
			tsPath := ""
			//读取m3u8 文件，并获取里面符合条件的ts
			err = utils.FileReadLineString(filepath.Join(dir, filename), func(line string) error {
				if strings.Contains(line, "#EXTINF") {
					strEXTINF = line
				} else if ts, _err := ParseM3u8TsName(line, m3.localTimeZone); _err == nil {
					//在当前指定时间段的
					if tr.Contains(ts.Time()) {
						bFind = true
						tsPath = "../" + line
					}
				} else {
					bFind = false
				}
				if bFind && strEXTINF != "" {
					tsList = append(tsList, [2]string{strEXTINF, tsPath})
					bFind = false
					tsPath = ""
					strEXTINF = ""
				}
				return nil
			})
			if err != nil && err != io.EOF {
				return err
			}
		}
		return nil
	})
	if err == nil && len(tsList) > 0 {
		res, err = m3.GenM3u8fileByMinutes(deviceId, tsList, minutes)
	}
	return
}

type HistoryFile struct {
	Video    string `json:"video"`
	Img      string `json:"img"`
	Duration int    `json:"duration"` // 秒
}

// GenM3u8fileByMinutes 生成m3u8文件按照指定的时长
func (m3 *M3u8) GenM3u8fileByMinutes(deviceId string, tsList [][2]string, minutes int) (fileList []HistoryFile, err error) {
	fileList = make([]HistoryFile, 0)
	searchDir, err := m3.getOrCreateCamDir(deviceId, "search")
	if err != nil {
		return
	}
	//tslist 一个就是一分钟
	tsArr := slices.Split(tsList, int64(minutes))
	//一组一个文件
	for i := range tsArr {
		//默认头信息
		data := []string{"#EXTM3U", "#EXT-X-PLAYLIST-TYPE:EVENT", "#EXT-X-VERSION:4", "#EXT-X-TARGETDURATION:65", "#EXT-X-MEDIA-SEQUENCE:0"}
		for j := range tsArr[i] {
			data = append(data, tsArr[i][j][:]...)
		}
		firstTs := tsArr[i][0][1]                                                                           //第一个ts名称
		tmpFile := ParseTsDatetime(firstTs) + "-" + ParseTsDatetime(tsArr[i][len(tsArr[i])-1][1]) + ".m3u8" // YYmmddHHMMSS-YYmmddHHMMSS
		_err := os.WriteFile(filepath.Join(searchDir, tmpFile), []byte(strings.Join(data, "\n")), 0664)
		if _err != nil {
			m3.log.Error("GenM3u8fileByMinutes write file error", zap.String("tmpFile", tmpFile), zap.Error(err))
			continue
		}
		fileList = append(fileList, HistoryFile{
			filepath.Join("/video", deviceId, "search", tmpFile),
			filepath.Join("/video", deviceId, "search", firstTs) + ".jpg",
			len(tsArr[i]) * 60})
	}
	return fileList, nil
}

func (m3 *M3u8) getOrCreateCamDir(deviceId string, sub ...string) (tempDir string, err error) {
	tempDir = path.Join(append([]string{m3.CamRootPath, deviceId}, sub...)...)
	// 检测文件是否存在
	var b bool
	b, err = utils.DirExists(tempDir)
	if err != nil {
		return
	}
	if !b {
		if err = os.MkdirAll(tempDir, os.ModePerm); err != nil {
			return
		}
	}
	return
}

// foreachTs 遍历循环一个m3u8文件的.ts纪录, 处理方法fn返回err将会中断循环
func (m3 *M3u8) foreachTs(m3u8Path string, fn func(ts Ts) error) (err error) {
	err = utils.FileReadLineString(m3u8Path, func(line string) error {
		m3ts, _err := ParseM3u8TsName(line, m3.localTimeZone)
		//非ts跳过
		if _err != nil {
			return nil
		}
		if _err = fn(m3ts); _err != nil {
			return _err
		}
		return nil
	})
	if err != nil && err != io.EOF {
		return err
	}
	return nil
}

func (m3 *M3u8) getVideoTime(m3u8File string) (int32, error) {
	content, _ := os.ReadFile(m3u8File)
	lines := strings.Split(string(content), "\n")
	d := 0.0
	for _, line := range lines {
		if strings.Contains(line, "#EXTINF:") {
			s := strings.ReplaceAll(line, "\n", "")
			s = strings.ReplaceAll(s, "#EXTINF:", "")
			s = strings.ReplaceAll(s, ",", "")
			f, err := strconv.ParseFloat(s, 32)
			if err == nil {
				d = d + f
			}
		}
	}
	return int32(d), nil
}

// foreachM3u8File 遍历循环一个目录下的m3u8文件进行处理, 处理方法fn返回err将会中断循环
func (m3 *M3u8) foreachM3u8File(m3u8Dir string, fn func(filename string, dir string) error) error {
	b, err := utils.DirExists(m3u8Dir)
	if err != nil {
		return err
	}
	if !b {
		return ErrorM3u8DirNotExists
	}
	res, err := os.ReadDir(m3u8Dir)
	if err != nil {
		return err
	}
	for i := range res {
		//非m3u8文件跳过
		if res[i].IsDir() || filepath.Ext(res[i].Name()) != ".m3u8" {
			continue
		}
		if _err := fn(res[i].Name(), m3u8Dir); _err != nil {
			return _err
		}
	}
	return nil
}
